/*
 * Copyright 2006 Robert Hanson <iamroberthanson AT gmail.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ruru.client.backend;

/**
 * <dl>
 * <dt><b>Title: </b><dd>Decimal Format</dd>
 * <p>
 * <dt><b>Description: </b><dd>This is a simple number formatting/ parsing class. Besides the simple number formatting
 * it also interprets shortcuts for thousand (k) million (m) and billion (b).<p/>
 * This Number Format class was adapted from the public domain javascript class found at 
 * http://www.mredkj.com/javascript/nfdocs.html </dd>
 * <p>
 * </dl>
 * @author <a href="mailto:jasone@greenrivercomputing.com">Jason Essington</a>
 * @version $Revision: 0.0 $
 */

public class NumberFormat
{
    public static final String COMMA = ",";
    public static final String PERIOD = ".";
    public static final char DASH = '-';
    public static final char LEFT_PAREN = '(';
    public static final char RIGHT_PAREN = ')';

    // k/m/b Shortcuts
    public static final String THOUSAND = "k";
    public static final String MILLION = "m";
    public static final String BILLION = "b";
    
    // currency position constants
    public static final int CUR_POS_LEFT_OUTSIDE = 0;
    public static final int CUR_POS_LEFT_INSIDE = 1;
    public static final int CUR_POS_RIGHT_INSIDE = 2;
    public static final int CUR_POS_RIGHT_OUTSIDE = 3;

    // negative format constants
    public static final int NEG_LEFT_DASH = 0;
    public static final int NEG_RIGHT_DASH = 1;
    public static final int NEG_PARENTHESIS = 2;

    // constant to signal that fixed precision is not to be used
    public static final int ARBITRARY_PRECISION = -1;

    private String inputDecimalSeparator = PERIOD; // decimal character used on the original string

    private boolean showGrouping = true;
    private String groupingSeparator = COMMA; // thousands grouping character
    private String decimalSeparator = PERIOD; // decimal point character

    private boolean showCurrencySymbol = false;
    private String currencySymbol = "$";
    private int currencySymbolPosition = CUR_POS_LEFT_OUTSIDE;

    private int negativeFormat = NEG_LEFT_DASH;
    private boolean isNegativeRed = false; // wrap the output in html that will display red?

    private int decimalPrecision = 0;
    private boolean useFixedPrecision = false;
    private boolean truncate = false; // truncate to decimalPrecision rather than rounding? 

    private boolean isPercentage = false; // should the result be displayed as a percentage?

    private NumberFormat()
    {
    }

    /**
     * returns the default instance of NumberFormat
     * @return
     */
    public static NumberFormat getInstance ()
    {
        NumberFormat nf = new NumberFormat();
        return nf;
    }

    /**
     * Returns a currency instance of number format
     * @return
     */
    public static NumberFormat getCurrencyInstance ()
    {
        return getCurrencyInstance("$", true);
    }

    /**
     * Returns a currency instance of number format that uses curSymbol as the currency symbol
     * @param curSymbol
     * @return
     */
    public static NumberFormat getCurrencyInstance (String curSymbol)
    {
        return getCurrencyInstance(curSymbol, true);
    }

    /**
     * Returns a currency instance of number format that uses curSymbol as the currency symbol 
     * and either commas or periods as the thousands separator.
     * @param curSymbol Currency Symbol
     * @param useCommas true, uses commas as the thousands separator, false uses periods
     * @return
     */
    public static NumberFormat getCurrencyInstance (String curSymbol, boolean useCommas)
    {
        NumberFormat nf = new NumberFormat();
        nf.isCurrency(true);
        nf.setCurrencySymbol(curSymbol);
        if (!useCommas) {
            nf.setDecimalSeparator(COMMA);
            nf.setGroupingSeparator(PERIOD);
        }
        nf.setFixedPrecision(2);
        return nf;
    }

    /**
     * Returns an instance that formats numbers as integers.
     * @return
     */
    public static NumberFormat getIntegerInstance ()
    {
        NumberFormat nf = new NumberFormat();
        nf.setShowGrouping(false);
        nf.setFixedPrecision(0);
        return nf;
    }

    public static NumberFormat getPercentInstance ()
    {
        NumberFormat nf = new NumberFormat();
        nf.isPercentage(true);
        nf.setFixedPrecision(2);
        nf.setShowGrouping(false);
        return nf;
    }

    public String format (String num)
    {
        return toFormatted(parse(num));
    }

    public double parse (String num)
    {
        return asNumber(num, inputDecimalSeparator);
    }

    /**
     * Static routine that attempts to create a double out of the
     * supplied text. This routine is a bit smarter than Double.parseDouble()
     * @param num
     * @return
     */
    public static double parseDouble (String num, String decimalChar)
    {
        return asNumber(num, decimalChar);
    }

    public static double parseDouble (String num)
    {
        return parseDouble(num, PERIOD);
    }

    public void setInputDecimalSeparator (String val)
    {
        inputDecimalSeparator = val == null ? PERIOD : val;
    }

    public void setNegativeFormat (int format)
    {
        negativeFormat = format;
    }

    public void setNegativeRed (boolean isRed)
    {
        isNegativeRed = isRed;
    }

    public void setShowGrouping (boolean show)
    {
        showGrouping = show;
    }

    public void setDecimalSeparator (String separator)
    {
        decimalSeparator = separator;
    }

    public void setGroupingSeparator (String separator)
    {
        groupingSeparator = separator;
    }

    public void isCurrency (boolean isC)
    {
        showCurrencySymbol = isC;
    }

    public void setCurrencySymbol (String symbol)
    {
        currencySymbol = symbol;
    }

    public void setCurrencyPosition (int cp)
    {
        currencySymbolPosition = cp;
    }

    public void isPercentage (boolean pct)
    {
        isPercentage = pct;
    }

    /**
     * Sets the number of fixed precision decimal places should be displayed.
     * To use arbitrary precision, setFixedPrecision(NumberFormat.ARBITRARY_PRECISION)
     * @param places 
     */
    public void setFixedPrecision (int places)
    {
        useFixedPrecision = places != ARBITRARY_PRECISION;
        this.decimalPrecision = places < 0 ? 0 : places;
    }

    /**
     * Causes the number to be truncated rather than rounded to its fixed precision.
     * @param trunc
     */
    public void setTruncate (boolean trunc)
    {
        truncate = trunc;
    }

    /**
     * 
     * @param preSep raw number as text
     * @param PERIOD incoming decimal point
     * @param decimalSeparator outgoing decimal point
     * @param groupingSeparator thousands separator
     * @return
     */
    private String addSeparators (String preSep)
    {
        String nStr = preSep;
        int dpos = nStr.indexOf(PERIOD);
        String nStrEnd = "";
        if (dpos != -1) {
            nStrEnd = decimalSeparator + nStr.substring(dpos + 1, nStr.length());
            nStr = nStr.substring(0, dpos);
        }
        int l = nStr.length();
        for (int i = l; i > 0; i--) {
            nStrEnd = nStr.charAt(i - 1) + nStrEnd;
            if (i != 1 && ((l - i + 1) % 3) == 0) nStrEnd = groupingSeparator + nStrEnd;
        }
        return nStrEnd;
    }

    protected String toFormatted(double num)
    {
        String nStr;

        if (isPercentage) num = num * 100;

        nStr = useFixedPrecision ? toFixed(Math.abs(getRounded(num)), decimalPrecision) : Double.toString(num);

        nStr = showGrouping ? addSeparators(nStr) : nStr.replaceAll("\\" + PERIOD, decimalSeparator);

        String c0 = "";
        String n0 = "";
        String c1 = "";
        String n1 = "";
        String n2 = "";
        String c2 = "";
        String n3 = "";
        String c3 = "";

        String negSignL = "" + ((negativeFormat == NEG_PARENTHESIS) ? LEFT_PAREN : DASH);
        String negSignR = "" + ((negativeFormat == NEG_PARENTHESIS) ? RIGHT_PAREN : DASH);
        
        if (currencySymbolPosition == CUR_POS_LEFT_OUTSIDE) {
            if (num < 0) {
                if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n1 = negSignL;
                if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n2 = negSignR;
            }
            if (showCurrencySymbol) c0 = currencySymbol;
        }
        else if (currencySymbolPosition == CUR_POS_LEFT_INSIDE) {
            if (num < 0) {
                if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n0 = negSignL;
                if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n3 = negSignR;
            }
            if (showCurrencySymbol) c1 = currencySymbol;
        }
        else if (currencySymbolPosition == CUR_POS_RIGHT_INSIDE) {
            if (num < 0) {
                if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n0 = negSignL;
                if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n3 = negSignR;
            }
            if (showCurrencySymbol) c2 = currencySymbol;
        }
        else if (currencySymbolPosition == CUR_POS_RIGHT_OUTSIDE) {
            if (num < 0) {
                if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n1 = negSignL;
                if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
                    n2 = negSignR;
            }
            if (showCurrencySymbol) c3 = currencySymbol;
        }
        nStr = c0 + n0 + c1 + n1 + nStr + n2 + c2 + n3 + c3 + (isPercentage ? "%" : "");

        if (isNegativeRed && num < 0) {
            nStr = "<font color='red'>" + nStr + "</font>";
        }

        return nStr;
   }

    /**
     * javascript only rounds to whole numbers, so we need to shift our decimal right, 
     * then round, then shift the decimal back left.
     * @param val
     * @return
     */
    private double getRounded (double val)
    {
        double exp = Math.pow(10, decimalPrecision);
        double rounded = val * exp;
        if (truncate)
            rounded = rounded >= 0 ? Math.floor(rounded) : Math.ceil(rounded);
        else
            rounded = Math.round(rounded);
        return rounded / exp;
    }

    private static native String toFixed(double val, int places) /*-{
        return val.toFixed(places);
    }-*/;

    private static double asNumber(String val, String inputDecimalValue)
    {
        String newVal = val;
        boolean isPercentage = false;
        // remove % if there is one
        if (newVal.indexOf('%') != -1) {
            newVal = newVal.replaceAll("\\%", "");
            isPercentage = true;
        }

        // convert abbreviations for thousand, million and billion
        newVal = newVal.toLowerCase().replaceAll(BILLION, "000000000");
        newVal = newVal.replaceAll(MILLION, "000000");
        newVal = newVal.replaceAll(THOUSAND, "000");

        // remove any characters that are not digit, decimal separator, +, -, (, ), e, or E      
        String re = "[^\\" + inputDecimalValue + "\\d\\-\\+\\(\\)eE]";
        newVal = newVal.replaceAll(re, "");

        // ensure that the first decimal separator is a . and remove the rest.
        int index = newVal.indexOf(inputDecimalValue);
        if (index != -1) {
            newVal = newVal.substring(0, index)
                    + PERIOD
                    + (newVal.substring(index + inputDecimalValue.length()).replaceAll("\\"
                            + inputDecimalValue, ""));
        }

        // convert right dash and paren negatives to left dash negative
        if (newVal.charAt(newVal.length() - 1) == DASH) {
            newVal = newVal.substring(0, newVal.length() - 1);
            newVal = DASH + newVal;
        }
        else if (newVal.charAt(0) == LEFT_PAREN
                && newVal.charAt(newVal.length() - 1) == RIGHT_PAREN) {
            newVal = newVal.substring(1, newVal.length() - 1);
            newVal = DASH + newVal;
        }

        Double parsed;
        try {
            parsed = new Double(newVal);
            if (parsed.isInfinite() || parsed.isNaN()) parsed = new Double(0);
        }
        catch (NumberFormatException e) {
            parsed = new Double(0);
        }

        return isPercentage ? parsed.doubleValue() / 100 : parsed.doubleValue();
    }
}

